"use strict"
import * as buffer from 'buffer';
import * as crypto from 'crypto';
import { apducmd } from "./apducmd";
import * as admzip from 'adm-zip';

const GP_DEFAULT_KEY = new Buffer("404142434445464748494A4B4C4D4E4F", "hex");
const GP_DEFAULT_ICV = new Buffer("0000000000000000", "hex");

export enum GP_SECURITY_LEVEL {
    NO_SECURITY = 0,
    COMMAND_MAC = 1,
    COMMAND_MAC_EN = 3
}

export interface IApdu {
    transmit(command: Buffer): Promise<Buffer>;
}

export interface gp_key_info {
    key_enc: Buffer;
    key_mac: Buffer;
    key_dek: Buffer;
    key_ver: number;
}

export const defaultkey = {
    key_enc: GP_DEFAULT_KEY,
    key_mac: GP_DEFAULT_KEY,
    key_dek: GP_DEFAULT_KEY,
    key_ver: 0
};

export enum GP_PROTOCOL {
    SCP_01 = 0,
    SCP_02 = 1
}

export class jsgp {
    private apdu: IApdu;
    private key_enc: Buffer;
    private key_mac: Buffer;
    private key_dek: Buffer;
    private session_key_enc: Buffer;
    private session_key_mac: Buffer;
    private session_key_dek: Buffer;
    private security_level: number;
    private command_mac: Buffer;
    private apducmd: apducmd;
    private protocol: GP_PROTOCOL;
    private store_num: number;
    private capcomponents: string[];

    constructor(ap: IApdu) {
        this.apdu = ap;
        this.key_enc = GP_DEFAULT_KEY;
        this.key_mac = GP_DEFAULT_KEY;
        this.key_dek = GP_DEFAULT_KEY;
        this.protocol = GP_PROTOCOL.SCP_02;

        this.apducmd = new apducmd();
        this.capcomponents = [];
        this.capcomponents.push("Header.cap");
        this.capcomponents.push("Directory.cap");
        this.capcomponents.push("Import.cap");
        this.capcomponents.push("Class.cap");
        this.capcomponents.push("Method.cap");
        this.capcomponents.push("StaticField.cap");
        this.capcomponents.push("Export.cap");
        this.capcomponents.push("ConstantPool.cap");
        this.capcomponents.push("RefLocation.cap");
        this.capcomponents.push("Descriptor.cap");
        //this.capcomponents.push("Debug.cap");

    }
    public gp_security_ch_01(security_level: number, key: gp_key_info = defaultkey): Promise<Buffer> {
        this.key_enc = key.key_enc;
        this.key_mac = key.key_mac;
        this.protocol = GP_PROTOCOL.SCP_01;

        let command = new Buffer(13);
        command.writeUInt16BE(0x8050, 0);
        command.writeInt8(security_level, 2);
        command.write("00081122334455667788", 3, 20, "hex");
        return this.apdu.transmit(command).then((initresp: Buffer) => {
            if (initresp.length != 30)
                throw new Error(`initial update resp length error=${initresp.length.toString(16)}`);
            if (initresp.readUInt16BE(initresp.length - 2) != 0x9000)
                throw new Error(`initial update resp status errro 0x${initresp.readUInt16BE(initresp.length - 2).toString(16)}`);

            let cardkeydiver = initresp.slice(0, 10);
            let cardkeyinfo = initresp.slice(10, 12);
            let cardchallenge = initresp.slice(12, 20);
            let cardcrypto = initresp.slice(20, 28);

            let DerivationData = new Buffer(16);
            cardchallenge.copy(DerivationData, 0, 4);
            DerivationData.write("11223344", 4, 8, "hex");
            cardchallenge.copy(DerivationData, 8, 0, 4);
            DerivationData.write("55667788", 12, 8, "hex");
            this.session_key_enc = this.crypt_des_ecb(DerivationData, this.key_enc);
            this.session_key_mac = this.crypt_des_ecb(DerivationData, this.key_mac);

            let data = new Buffer(24);
            data.write("1122334455667788", 0, 16, "hex");
            cardchallenge.copy(data, 8);
            data.write("8000000000000000", 16, 16, "hex");
            let hostcardcrypto = this.crypt_des_cbc(data, this.session_key_enc);
            hostcardcrypto = hostcardcrypto.slice(hostcardcrypto.length - 8);
            if (!hostcardcrypto.equals(cardcrypto))
                throw new Error(`hostcard crypto not cardcrypto`);

            cardchallenge.copy(data, 0);
            data.write("1122334455667788", 8, 16, "hex");
            data.write("8000000000000000", 16, 16, "hex");
            let hostcrypto = this.crypt_des_cbc(data, this.session_key_enc);
            hostcrypto = hostcrypto.slice(hostcrypto.length - 8);

            data = new Buffer(21);
            data.writeUInt16BE(0x8482, 0);
            data.writeUInt8(2, security_level);
            data.writeUInt16BE(3, 0x0010);
            hostcrypto.copy(data, 5);
            this.command_mac = this.gp_data_mac(data.slice(0, 13));
            this.command_mac.copy(data, 13);

            return this.apdu.transmit(data).then((recv: Buffer) => {
                if (recv.length != 2)
                    throw new Error(`external command resp length error=${recv.length.toString(16)}`);
                if (recv.readUInt16BE(0) != 0x9000)
                    throw new Error(`external command resp status error=${recv.readUInt16BE(0).toString(16)}`);
                this.security_level = security_level;
                this.store_num = 0;
                return recv;
            });
        });

    }
    public gp_security_ch_02(security_level: number, key: gp_key_info = defaultkey): Promise<Buffer> {
        this.key_enc = key.key_enc;
        this.key_mac = key.key_mac;
        this.key_dek = key.key_dek;
        this.protocol = GP_PROTOCOL.SCP_02;

        let command = new Buffer(13);
        command.writeUInt16BE(0x8050, 0);
        command.writeInt8(security_level, 2);
        command.write("00081122334455667788", 3, 20, "hex");
        return this.apdu.transmit(command).then((initresp: Buffer) => {
            if (initresp.length != 30)
                throw new Error(`initial update resp length error=${initresp.length.toString(16)}`);
            if (initresp.readUInt16BE(initresp.length - 2) != 0x9000)
                throw new Error(`initial update resp status errro 0x${initresp.readUInt16BE(initresp.length - 2).toString(16)}`);

            let cardkeydiver = initresp.slice(0, 10);
            let cardkeyinfo = initresp.slice(10, 12);
            let cardseqnum = initresp.readUInt16BE(12);
            let cardchallenge = initresp.slice(14, 20);
            let cardcrypto = initresp.slice(20, 28);

            let data = new Buffer(16);
            data.writeUInt16BE(cardseqnum, 2);
            data.write("000000000000000000000000", 4, 14, "hex");
            data.write("0182", 0, 4, "hex");
            this.session_key_enc = this.crypt_des_cbc(data, this.key_enc);
            data.write("0101", 0, 4, "hex");
            this.session_key_mac = this.crypt_des_cbc(data, this.key_mac);
            data.write("0181", 0, 4, "hex");
            this.session_key_dek = this.crypt_des_cbc(data, this.key_dek);

            data = new Buffer(24);
            data.write("1122334455667788", 0, 16, "hex");
            data.writeUInt16BE(cardseqnum, 8);
            cardchallenge.copy(data, 10);
            data.write("8000000000000000", 16, 16, "hex");
            let hostcardcrypto = this.crypt_des_cbc(data, this.session_key_enc);
            hostcardcrypto = hostcardcrypto.slice(hostcardcrypto.length - 8);
            if (!hostcardcrypto.equals(cardcrypto))
                throw new Error(`hostcard crypto not cardcrypto`);

            data.writeUInt16BE(cardseqnum, 0);
            cardchallenge.copy(data, 2);
            data.write("1122334455667788", 8, 16, "hex");
            data.write("8000000000000000", 16, 16, "hex");
            let hostcrypto = this.crypt_des_cbc(data, this.session_key_enc);
            hostcrypto = hostcrypto.slice(hostcrypto.length - 8);

            data = new Buffer(21);
            data.writeUInt16BE(0x8482, 0);
            data.writeUInt8(2, security_level);
            data.writeUInt16BE(3, 0x0010);
            hostcrypto.copy(data, 5);
            this.command_mac = this.gp_data_mac(data.slice(0, 13));
            this.command_mac.copy(data, 13);
            return this.apdu.transmit(data).then((recv: Buffer) => {
                if (recv.length != 2)
                    throw new Error(`external command resp length error=${recv.length.toString(16)}`);
                if (recv.readUInt16BE(0) != 0x9000)
                    throw new Error(`external command resp status error=${recv.readUInt16BE(0).toString(16)}`);
                this.security_level = security_level;
                this.store_num = 0;
                return recv;
            });

        });

    }

    public gp_select(p1p2: number, name: Buffer): Promise<Buffer> {
        let command = new Buffer(name.length + 5);
        command.writeUInt16BE(0x00A4, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(name.length, 4);
        name.copy(command, 5);
        return this.apdu.transmit(command);
    }

    public gp_select_aid(aid: Buffer): Promise<Buffer> {
        return this.gp_select(0x0400, aid);
    }

    public gp_delete(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80E4, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_delete_aid(p1p2: number, aid: Buffer): Promise<Buffer> {
        let data = new Buffer(aid.length + 2);
        data.writeUInt8(0x4f, 0);
        data.writeUInt8(aid.length, 1);
        return this.gp_delete(p1p2, data);
    }

    public gp_manage_ch(p1p2: number): Promise<Buffer> {
        let command = new Buffer(5);
        command.writeUInt8(0x00, 0);
        command.writeUInt8(0x70, 1);
        command.writeUInt8(p1p2, 2);
        command.writeUInt8(0, 4);
        if ((p1p2 & 0xff00) == 0x8000)
            command = command.slice(0, 4);
        return this.apdu.transmit(command);
    }

    public gp_install(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80E6, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_install_ex(p1p2: number, aid1: Buffer, aid2: Buffer, aid3: Buffer, pri: number, para: Buffer, token: Buffer): Promise<Buffer> {
        let command = new Buffer(265);
        let offset = 0;

        command.writeUInt16BE(0x80E6, 0);
        command.writeUInt16BE(p1p2, 2);

        // write start data field
        offset = 5;
        command.writeUInt8(aid1.length, offset);
        offset += 1;
        aid1.copy(command, offset);
        offset += aid1.length;
        command.writeUInt8(aid2.length, offset);
        offset += 1;
        aid2.copy(command, offset);
        offset += aid2.length;
        command.writeUInt8(aid3.length, offset);
        offset += 1;
        aid3.copy(command, offset);
        offset += aid3.length;
        if ((p1p2 & 0x0200) == 0x0000) {
            if (pri > 0x010000) {
                command.writeUInt8(0x03, offset);
                offset += 1;
                command.writeUInt8(pri >> 16, offset);
                offset += 1;
                command.writeUInt8(pri >> 8, offset);
                offset += 1;
                command.writeUInt8(pri >> 0, offset);
                offset += 1;
            }
            else {
                command.writeUInt8(0x01, offset);
                offset += 1;
                command.writeUInt8(pri, offset);
                offset += 1;
            }
        }
        command.writeUInt8(para.length, offset);
        offset += 1;
        para.copy(command, offset);
        offset += para.length;
        command.writeUInt8(token.length, offset);
        offset += 1;
        token.copy(command, offset);
        offset += token.length;
        // write lc
        command.writeUInt8(4, offset - 5);

        return this.gp_cmd_mac(command.slice(0, offset));
    }

    public gp_load(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80E8, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_storedata(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80E2, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public async gp_storedgi(p1: number, dgi: number, dgidata: Buffer): Promise<Buffer> {
        let command = new Buffer(265);
        let data = new Buffer(dgidata.length + 20);
        let offset = 0;
        let transoff = 0;
        let translen = 0;
        let recv: Buffer;

        data.writeUInt16BE(dgi, offset);
        offset += 1;
        if (dgidata.length < 0xff) {
            data.writeUInt8(dgidata.length, offset);
            offset += 1;
        }
        else {
            data.writeUInt8(0xff, offset);
            offset += 1;
            data.writeUInt16BE(dgidata.length, offset);
            offset += 2;
        }
        dgidata.copy(data, offset);
        offset += dgidata.length;

        while (transoff < offset) {
            translen = offset - transoff;
            if (translen > 0xFE)
                translen = 0xFE;
            let cmddata = data.slice(transoff, transoff + translen);
            if ((p1 & 0x60) == 0x60) {
                let padded = this.data_padding_ex(cmddata);
                cmddata = this.crypt_des_ecb(padded, this.session_key_dek);
            }
            command.writeUInt16BE(0x80E2, 0);
            command.writeUInt8(p1, 2);
            command.writeUInt8(this.store_num, 3);
            command.writeUInt8(translen, 4);
            cmddata.copy(command, 5);
            recv = await this.gp_cmd_mac(command.slice(0, translen + 5));
            if (recv.readUInt16BE(recv.length - 2) != 0x9000)
                return recv;
            transoff += translen;
            this.store_num += 1;
        }
        return recv;
    }

    public gp_putkey(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80D8, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }


    public gp_getdata(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80CA, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_getstatus(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80F2, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_setstatus(p1p2: number, data: Buffer): Promise<Buffer> {
        let command = new Buffer(data.length + 5);
        command.writeUInt16BE(0x80F0, 0);
        command.writeUInt16BE(p1p2, 2);
        command.writeUInt8(data.length, 4);
        data.copy(command, 5);
        return this.gp_cmd_mac(command);
    }

    public gp_cap_data(capfile: string): Buffer {
        let capdata: Buffer[] = [];
        let zip = new admzip(capfile);
        let zipEntries = zip.getEntries();

        this.capcomponents.forEach(comp => {
            zipEntries.forEach(zipEntry => {
                if (this.strendswith(zipEntry.entryName, comp)) {
                    capdata.push(zipEntry.getCompressedData());
                }
            });
        });
        return Buffer.concat(capdata);
    }

    public gp_cap_pkg_aid(capfile: string): Buffer {
        let pkgaid: Buffer;
        let zip = new admzip(capfile);
        let zipEntries = zip.getEntries();

        this.capcomponents.forEach(comp => {
            zipEntries.forEach(zipEntry => {
                if (this.strendswith(zipEntry.entryName, "Header.cap")) {
                    pkgaid = zipEntry.getCompressedData();
                    pkgaid = pkgaid.slice(13);
                }
            });
        });
        return pkgaid;
    }

    public async gp_load_cap(capdata: Buffer): Promise<Buffer> {
        let caplen = capdata.length;
        let buff = new Buffer(caplen + 5);
        let offset = 0;
        let transoff = 0;
        let translen = 0;
        let recv: Buffer;
        let load_num: number = 0;

        if (caplen > 65535) {
            buff.writeUInt16BE(0xc483, 0);
            buff.writeUInt16BE(caplen >> 8, 2);
            buff.writeUInt8(caplen, 4);
            offset += 5;
        }
        else if (caplen > 255) {
            buff.writeUInt16BE(0xc482, 0);
            buff.writeUInt16BE(caplen, 2);
            offset += 4;
        }
        else if (caplen > 127) {
            buff.writeUInt16BE(0xc481, 0);
            buff.writeUInt8(caplen, 2);
            offset += 3;
        }
        else {
            buff.writeUInt8(0xc4, 0);
            buff.writeUInt8(caplen, 1);
            offset += 2;
        }
        capdata.copy(buff, offset);
        offset += caplen;

        while (transoff < offset) {
            translen = offset - transoff;
            let P1 = 0x80;
            if (translen > 230) {
                translen = 230;
                P1 = 0x00;
            }
            let cmddata = buff.slice(transoff, transoff + translen);
            recv = await this.gp_load((P1 << 8) | (load_num & 0xff), cmddata);
            if (recv.readUInt16BE(recv.length - 2) != 0x9000)
                return recv;
            transoff += translen;
            load_num += 1;
        }
        return recv;
    }

    public gp_cmd_mac(cmd: Buffer): Promise<Buffer> {
        let command: Buffer;
        this.apducmd.parse(cmd);

        if (this.security_level == GP_SECURITY_LEVEL.NO_SECURITY) {
            command = new Buffer(cmd.length);
            this.apducmd.CLA &= 0xFB;
            command.writeUInt8(this.apducmd.CLA, 0);
            cmd.slice(1).copy(command, 1);
        }
        else if (this.security_level == GP_SECURITY_LEVEL.COMMAND_MAC) {
            command = new Buffer(cmd.length + 16);
            this.apducmd.CLA |= 0x04;
            this.apducmd.LC += 8;
            this.command_mac.copy(command, 0);
            command.writeUInt8(this.apducmd.CLA, 8);
            cmd.slice(1, 4).copy(command, 9);
            command.writeUInt8(this.apducmd.LC, 12);
            this.apducmd.DATA.copy(command, 13);
            this.command_mac = this.gp_data_mac(command.slice(0, this.apducmd.DATA.length + 13));
            this.command_mac.copy(command, this.apducmd.DATA.length + 13);
            if (this.apducmd.LE != undefined)
                command.writeUInt8(this.apducmd.LE, command[12] + 13)
            command = command.slice(8);
        }
        else if (this.security_level == GP_SECURITY_LEVEL.COMMAND_MAC_EN) {
            command = new Buffer(cmd.length + 24);
            this.apducmd.CLA |= 0x04;
            this.apducmd.LC += 8;
            this.command_mac.copy(command, 0);
            command.writeUInt8(this.apducmd.CLA, 8);
            cmd.slice(1, 4).copy(command, 9);
            command.writeUInt8(this.apducmd.LC, 12);
            this.apducmd.DATA.copy(command, 13);
            this.command_mac = this.gp_data_mac(command.slice(0, this.apducmd.DATA.length + 13));
            let crypt = this.gp_data_encrypt(this.apducmd.DATA);
            crypt.copy(command, 13);
            this.command_mac.copy(command, crypt.length + 13);
            command.writeUInt8(crypt.length + 8, 12);
            if (this.apducmd.LE != undefined) {
                command.writeUInt8(this.apducmd.LE, command[12] + 13)
                command = command.slice(8, command[12] + 14);
            }
            else {
                command = command.slice(8, command[12] + 13);
            }
        }
        return this.apdu.transmit(command);
    }

    protected crypt_des_ecb(msg: Buffer, key: Buffer, en: boolean = true): Buffer {
        let cipher;
        let Keys = [];
        let Result = [];

        Keys.push(key);
        if (key.length == 16)
            Keys.push(key.subarray(0, 8));
        if (en === true)
            if (key.length == 8)
                cipher = crypto.createCipheriv("des-ecb", Buffer.concat(Keys), "");
            else
                cipher = crypto.createCipheriv("des-ede3", Buffer.concat(Keys), "");
        else
            if (key.length == 8)
                cipher = crypto.createDecipheriv("des-ecb", Buffer.concat(Keys), "");
            else
                cipher = crypto.createDecipheriv("des-ede3", Buffer.concat(Keys), "");
        cipher.setAutoPadding(false);
        Result.push(cipher.update(msg));
        Result.push(cipher.final());
        return Buffer.concat(Result);
    }
    protected crypt_des_cbc(msg: Buffer, key: Buffer, iv: Buffer = GP_DEFAULT_ICV, en: boolean = true): Buffer {
        let cipher;
        let Keys = [];
        let Results = [];
        Keys.push(key);
        if (key.length == 16)
            Keys.push(key.subarray(0, 8));
        if (en === true)
            if (key.length == 8)
                cipher = crypto.createCipheriv("des-cbc", Buffer.concat(Keys), iv);
            else
                cipher = crypto.createCipheriv("des-ede3-cbc", Buffer.concat(Keys), iv);
        else
            if (key.length == 8)
                cipher = crypto.createDecipheriv("des-cbc", Buffer.concat(Keys), iv);
            else
                cipher = crypto.createDecipheriv("des-ede3-cbc", Buffer.concat(Keys), iv);
        cipher.setAutoPadding(false);
        Results.push(cipher.update(msg));
        Results.push(cipher.final());
        return Buffer.concat(Results);
    }

    public gp_data_mac(data: Buffer): Buffer {
        let padded = this.data_padding(data);
        if (this.protocol == GP_PROTOCOL.SCP_02) {
            let mac0 = this.crypt_des_cbc(padded, this.command_mac.slice(0, 8), GP_DEFAULT_ICV);
            let mac1 = new Buffer(mac0.subarray(mac0.length - 16));
            let mac2 = this.crypt_des_ecb(mac1, this.command_mac.slice(16, 16), false);
            return this.crypt_des_ecb(mac2, this.command_mac.slice(0, 16));
        }
        else {
            let mac0 = this.crypt_des_cbc(padded, this.command_mac, GP_DEFAULT_ICV);
            return mac0.slice(mac0.length - 8);
        }
    }
    public gp_data_encrypt(data: Buffer): Buffer {
        let padded = this.data_padding(data);

        if (this.protocol == GP_PROTOCOL.SCP_02) {
            return this.crypt_des_cbc(padded, this.session_key_enc);
        }
        else{

        }
    }
    public gp_sensitive_encrypt(data: Buffer): Buffer {
        let padded = this.data_padding_ex(data);
        if (this.protocol == GP_PROTOCOL.SCP_02) {
            return this.crypt_des_ecb(padded, this.session_key_dek);
        }
        else{
            
        }
    }

    protected data_padding(data: Buffer): Buffer {
        let padlen = data.length % 8;
        if (padlen == 0)
            padlen = 8;
        let result = new Buffer(data.length + padlen);
        data.copy(result);
        result.writeUInt8(0x80, data.length);
        result.fill(0x00, data.length + 1);
        return result;
    }
    protected data_padding_ex(data: Buffer): Buffer {
        let padlen = data.length % 8;
        if (padlen == 0)
            return data;
        let result = new Buffer(data.length + padlen);
        data.copy(result);
        result.writeUInt8(0x80, data.length);
        result.fill(0x00, data.length + 1);
        return result;
    }
    protected strendswith(s: string, t: string) {
        return (s.lastIndexOf(t) == (s.length - t.length));
    }
    protected strstartswith(s: string, t: string) {
        return (s.indexOf(t) == 0);
    }

}



